package hkust.ee.fyp.drawer;

import java.util.ArrayList;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;

public class SketchPadView extends View implements IUndoCommand {
	public static final int STROKE_NONE = 0;
	public static final int STROKE_PEN = 1;
	public static final int STROKE_ERASER = 2;
	public static final int UNDO_SIZE = 20;

	private boolean m_isEnableDrawStroke = true;
	private boolean m_isDirty = false;
	private boolean m_isTouchUp = false;
	private boolean m_isSetForeBmp = false;
	private int m_bkColor = Color.WHITE;

	private int m_strokeType = STROKE_PEN;
	private int m_strokeColor = Color.BLACK;
	private int m_penSize = CommonDef.MIDDLE_PEN_WIDTH;
	private int m_eraserSize = CommonDef.MIDDLE_ERASER_WIDTH;
	private int m_canvasWidth = 100;
	private int m_canvasHeight = 100;
	private boolean m_canClear = true;

	private Paint m_bitmapPaint = null;
	private Bitmap m_foreBitmap = null;
	private SketchPadUndoStack m_undoStack = null;
	private Bitmap m_tempForeBitmap = null;
	private Bitmap m_bkBitmap = null;
	
	private RectF canvas_dst = null;
	private Rect bkBitmap_rst = null;
	
	private Canvas m_canvas = null;
	private ISketchPadTool m_curTool = null;
	private ISketchPadCallback m_callback = null;

	private ArrayList<Bitmap> m_stickerBitmap = new ArrayList<Bitmap>();
	private ArrayList<BitmapPosition> m_stickerBitmapPosition = new ArrayList<BitmapPosition>();
	private boolean m_isEnableDragSticker = false;
	private boolean isStickerDragging = false;

	private GestureDetector gestures = null;
	
	private Bitmap m_photoframeBitmap = null;
	private Rect photoframe_rst = null;
	
	public SketchPadView(Context context) {
		this(context, null);
	}

	public SketchPadView(Context context, AttributeSet attrs) {
		super(context, attrs);
		initialize();
	}

	public SketchPadView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		initialize();
	}

	public boolean isDirty() {
		return m_isDirty;
	}

	public void enableStrokeDraw() {
		m_isEnableDrawStroke = true;
		m_isEnableDragSticker = false;
	}

	public void setBkColor(int color) {
		if (m_bkColor != color) {
			m_bkColor = color;
			invalidate();
		}
	}

	public void setForeBitmap(Bitmap foreBitmap) {
		if (foreBitmap != m_foreBitmap && null != foreBitmap) {
			// Recycle the bitmap.
			if (null != m_foreBitmap) {
				m_foreBitmap.recycle();
			}

			m_isSetForeBmp = true;

			// Remember the temporary fore bitmap.
			m_tempForeBitmap = BitmapUtil.duplicateBitmap(foreBitmap);

			// Here create a new fore bitmap to avoid crashing when set bitmap
			// to canvas.
			m_foreBitmap = BitmapUtil.duplicateBitmap(foreBitmap);
			if (null != m_foreBitmap && null != m_canvas) {
				m_canvas.setBitmap(m_foreBitmap);
			}

			m_canClear = true;

			invalidate();
		}
	}

	public Bitmap getForeBitmap() {
		return m_foreBitmap;
	}

	public void setBkBitmap(Bitmap bmp) {
		if (m_bkBitmap != bmp) {
			m_bkBitmap = bmp;
			bkBitmap_rst = new Rect(0, 0, m_bkBitmap.getWidth(),
					m_bkBitmap.getHeight());
			// Log.d("sketchpad", getWidth() + " " + getHeight() + " " + m_bkBitmap.getWidth() + " " + m_bkBitmap.getHeight());
 			invalidate();
		}
	}

	public Bitmap getBkBitmap() {
		return m_bkBitmap;
	}

	public void setStrokeType(int type) {
		switch (type) {
		case STROKE_PEN:
			m_curTool = new SketchPadPen(m_penSize, m_strokeColor);
			break;

		case STROKE_ERASER:
			m_curTool = new SketchPadEraser(m_eraserSize);
			break;
		}

		m_strokeType = type;
	}

	public void setStrokeSize(int size, int type) {
		switch (type) {
		case STROKE_PEN:
			m_penSize = size;
			break;

		case STROKE_ERASER:
			m_eraserSize = size;
			break;
		}
	}

	public void setStrokeColor(int color) {
		m_strokeColor = color;
	}

	public int getStrokeSize() {
		return m_penSize;
	}
	
	public void setPenSize(int penSize) {
		m_penSize = penSize;
	}

	public int getStrokeColor() {
		return m_strokeColor;
	}

	public void clearAllStrokes() {
		if (m_canClear) {
			// Clear the undo stack.
			m_undoStack.clearAll();

			// Recycle the temporary fore bitmap
			if (null != m_tempForeBitmap) {
				m_tempForeBitmap.recycle();
				m_tempForeBitmap = null;
			}

			// Create a new fore bitmap and set to canvas.
			createStrokeBitmap(m_canvasWidth, m_canvasHeight);

			invalidate();
			m_isDirty = true;
			m_canClear = false;
		}
	}

	public Bitmap getCanvasSnapshot() {
		setDrawingCacheEnabled(true);
		buildDrawingCache(true);
		Bitmap bmp = getDrawingCache(true);

		if (null == bmp) {
			android.util.Log.d("drawer",
					"getCanvasSnapshot getDrawingCache == null");
		}

		return BitmapUtil.duplicateBitmap(bmp);
	}

	public void setCallback(ISketchPadCallback callback) {
		m_callback = callback;
	}

	public ISketchPadCallback getCallback() {
		return m_callback;
	}

	@Override
	public void onDeleteFromRedoStack() {
		// Do nothing currently.
	}

	@Override
	public void onDeleteFromUndoStack() {
		// Do nothing currently.
	}

	@Override
	public void redo() {
		if (null != m_undoStack) {
			m_undoStack.redo();
		}
	}

	@Override
	public void undo() {
		if (null != m_undoStack) {
			m_undoStack.undo();
		}
	}

	@Override
	public boolean canUndo() {
		if (null != m_undoStack) {
			return m_undoStack.canUndo();
		}

		return false;
	}

	@Override
	public boolean canRedo() {
		if (null != m_undoStack) {
			return m_undoStack.canRedo();
		}

		return false;
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		gestures.onTouchEvent( event);
		
		if (null != m_callback) {
			int action = event.getAction();
			if (MotionEvent.ACTION_DOWN == action) {
				m_callback.onTouchDown(this, event);
			} else if (MotionEvent.ACTION_UP == action) {
				m_callback.onTouchUp(this, event);
			}
		}

		if (m_isEnableDragSticker) {
			BitmapPosition position;
			
			switch (event.getAction()) {
			case MotionEvent.ACTION_DOWN:
				isStickerDragging = false;
				position = new BitmapPosition( event.getX(), event.getY());
				for ( int i = m_stickerBitmap.size() - 1 ; i >= 0 ; i-- )
				{
					if ( m_stickerBitmapPosition.get(i).isDraggingMe(position))
					{
						Bitmap sticker = m_stickerBitmap.get( i);
						BitmapPosition pos = m_stickerBitmapPosition.get( i);
						
						m_stickerBitmap.remove( i);
						m_stickerBitmapPosition.remove( i);

						m_stickerBitmap.add( sticker);
						m_stickerBitmapPosition.add( pos);
						
						isStickerDragging = true;	
						invalidate();
						break;
					}
				}
				break;
			case MotionEvent.ACTION_MOVE:
				if ( isStickerDragging )
				{
					int count = m_stickerBitmap.size();
					Bitmap bitmap = m_stickerBitmap.get( count - 1);
					position = m_stickerBitmapPosition.get( count - 1);
					position.setX( event.getX() - bitmap.getWidth() / 2);
					position.setY( event.getY() - bitmap.getHeight() / 2);
					
					invalidate();
				}
				break;
			case MotionEvent.ACTION_UP:
				isStickerDragging = false;
				break;
			}
		}

		else if (m_isEnableDrawStroke) {
			m_isTouchUp = false;
			switch (event.getAction()) {
			case MotionEvent.ACTION_DOWN:
				// This will create a new stroke tool instance with specified
				// stroke type.
				// And the created tool will be added to undo stack.
				setStrokeType(m_strokeType);

				m_curTool.touchDown(event.getX(), event.getY());
				invalidate();
				break;

			case MotionEvent.ACTION_MOVE:
				m_curTool.touchMove(event.getX(), event.getY());
				// If current stroke type is eraser, draw strokes on bitmap hold
				// by m_canvas.
				if (STROKE_ERASER == m_strokeType) {
					m_curTool.draw(m_canvas);
				}

				invalidate();
				m_isDirty = true;
				m_canClear = true;
				break;

			case MotionEvent.ACTION_UP:
				m_isTouchUp = true;
				if (m_curTool.hasDraw()) {
					// Add to undo stack.
					m_undoStack.push(m_curTool);
				}

				m_curTool.touchUp(event.getX(), event.getY());
				// Draw strokes on bitmap which is hold by m_canvas.
				m_curTool.draw(m_canvas);

				invalidate();
				m_isDirty = true;
				m_canClear = true;
				break;
			}
		}

		// Here must return true if enable to draw, otherwise the stroke may NOT
		// be drawn.
		return true;
	}

	protected void setCanvasSize(int width, int height) {
		if (width > 0 && height > 0) {
			if (m_canvasWidth != width || m_canvasHeight != height) {
				m_canvasWidth = width;
				m_canvasHeight = height;

				createStrokeBitmap(m_canvasWidth, m_canvasHeight);
			}
		}
	}

	@Override
	protected void onDraw(Canvas canvas) {
		// Draw background color.
		canvas.drawColor(m_bkColor);

		// Draw background bitmap.
		if (null != m_bkBitmap) {
			canvas.drawBitmap(m_bkBitmap, bkBitmap_rst, canvas_dst, m_bitmapPaint);
		}

		if (null != m_stickerBitmap) {			
			for ( int i = 0 ; i < m_stickerBitmap.size() ; i++ )
			{
				BitmapPosition position = m_stickerBitmapPosition.get( i);
				canvas.drawBitmap( m_stickerBitmap.get( i), position.getX(), position.getY(), m_bitmapPaint);
			}
		}
		
		if (null != m_foreBitmap) {
			canvas.drawBitmap(m_foreBitmap, 0, 0, m_bitmapPaint);
		}

		if (null != m_curTool) {
			// Do NOT draw current tool stroke real time if stroke type is NOT
			// eraser, because
			// eraser is drawn on bitmap hold by m_canvas.
			if (STROKE_ERASER != m_strokeType) {
				// We do NOT draw current tool's stroke to canvas when ACTION_UP
				// event is occurring,
				// because the stroke has been drawn to bitmap hold by m_canvas.
				// But the tool will be
				// drawn if undo or redo operation is performed.
				if (!m_isTouchUp) {
					m_curTool.draw(canvas);
				}
			}
		}
		
		if (null != m_photoframeBitmap) {
			canvas.drawBitmap(m_photoframeBitmap, photoframe_rst, canvas_dst, m_bitmapPaint);
		}
	}

	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);

		// If NOT set fore bitmap, call setCanvasSize() method to set canvas
		// size, it will
		// create a new fore bitmap and set to canvas.
		if (!m_isSetForeBmp) {
			setCanvasSize(w, h);
		}

		m_canvasWidth = w;
		m_canvasHeight = h;

		canvas_dst = new RectF( 0, 0, m_canvasWidth, m_canvasHeight);
		m_isSetForeBmp = false;
	}

	protected void setTempForeBitmap(Bitmap tempForeBitmap) {
		if (null != tempForeBitmap) {
			if (null != m_foreBitmap) {
				m_foreBitmap.recycle();
			}

			m_foreBitmap = BitmapUtil.duplicateBitmap(tempForeBitmap);

			if (null != m_foreBitmap && null != m_canvas) {
				m_canvas.setBitmap(m_foreBitmap);
				invalidate();
			}
		}
	}

	protected void initialize() {
		m_canvas = new Canvas();
		m_bitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
		m_undoStack = new SketchPadUndoStack(this, UNDO_SIZE);
		// Set stroke type and create a stroke tool.
		setStrokeType(STROKE_PEN); 
	}
	
	public void setActivityRef( Activity drawerActivity)
	{
		gestures = new GestureDetector(drawerActivity, new GestureListener(this));
	}

	protected void createStrokeBitmap(int w, int h) {
		m_canvasWidth = w;
		m_canvasHeight = h;

		Bitmap bitmap = Bitmap.createBitmap(m_canvasWidth, m_canvasHeight,
				Bitmap.Config.ARGB_8888);
		if (null != bitmap) {
			m_foreBitmap = bitmap;
			// Set the fore bitmap to m_canvas to be as canvas of strokes.
			m_canvas.setBitmap(m_foreBitmap);
		}
	}

	public class SketchPadUndoStack {
		private int m_stackSize = 0;
		private SketchPadView m_sketchPad = null;
		private ArrayList<ISketchPadTool> m_undoStack = new ArrayList<ISketchPadTool>();
		private ArrayList<ISketchPadTool> m_redoStack = new ArrayList<ISketchPadTool>();
		private ArrayList<ISketchPadTool> m_removedStack = new ArrayList<ISketchPadTool>();

		public SketchPadUndoStack(SketchPadView sketchPad, int stackSize) {
			m_sketchPad = sketchPad;
			m_stackSize = stackSize;
		}

		public void push(ISketchPadTool sketchPadTool) {
			if (null != sketchPadTool) {
				if (m_undoStack.size() == m_stackSize && m_stackSize > 0) {
					ISketchPadTool removedTool = m_undoStack.get(0);
					m_removedStack.add(removedTool);
					m_undoStack.remove(0);
				}

				m_undoStack.add(sketchPadTool);
			}
		}

		public void clearAll() {
			m_redoStack.clear();
			m_undoStack.clear();
			m_removedStack.clear();
		}

		public void undo() {
			if (canUndo() && null != m_sketchPad) {
				ISketchPadTool removedTool = m_undoStack
						.get(m_undoStack.size() - 1);
				m_redoStack.add(removedTool);
				m_undoStack.remove(m_undoStack.size() - 1);

				if (null != m_tempForeBitmap) {
					// Set the temporary fore bitmap to canvas.
					m_sketchPad.setTempForeBitmap(m_sketchPad.m_tempForeBitmap);
				} else {
					// Create a new bitmap and set to canvas.
					m_sketchPad.createStrokeBitmap(m_sketchPad.m_canvasWidth,
							m_sketchPad.m_canvasHeight);
				}

				Canvas canvas = m_sketchPad.m_canvas;

				// First draw the removed tools from undo stack.
				for (ISketchPadTool sketchPadTool : m_removedStack) {
					sketchPadTool.draw(canvas);
				}

				for (ISketchPadTool sketchPadTool : m_undoStack) {
					sketchPadTool.draw(canvas);
				}

				m_sketchPad.invalidate();
			}
		}

		public void redo() {
			if (canRedo() && null != m_sketchPad) {
				ISketchPadTool removedTool = m_redoStack
						.get(m_redoStack.size() - 1);
				m_undoStack.add(removedTool);
				m_redoStack.remove(m_redoStack.size() - 1);

				if (null != m_tempForeBitmap) {
					// Set the temporary fore bitmap to canvas.
					m_sketchPad.setTempForeBitmap(m_sketchPad.m_tempForeBitmap);
				} else {
					// Create a new bitmap and set to canvas.
					m_sketchPad.createStrokeBitmap(m_sketchPad.m_canvasWidth,
							m_sketchPad.m_canvasHeight);
				}

				Canvas canvas = m_sketchPad.m_canvas;

				// First draw the removed tools from undo stack.
				for (ISketchPadTool sketchPadTool : m_removedStack) {
					sketchPadTool.draw(canvas);
				}

				for (ISketchPadTool sketchPadTool : m_undoStack) {
					sketchPadTool.draw(canvas);
				}

				m_sketchPad.invalidate();
			}
		}

		public boolean canUndo() {
			return (m_undoStack.size() > 0);
		}

		public boolean canRedo() {
			return (m_redoStack.size() > 0);
		}
	}

	public void addStickerBitmap(Bitmap bmp) {
		m_stickerBitmap.add( bmp);
		m_stickerBitmapPosition.add( new BitmapPosition( bmp.getWidth(), bmp.getHeight(), m_canvasWidth, m_canvasHeight));
	}
	
	public void enableStickerDrag()
	{
		m_isEnableDragSticker = true;
		m_isEnableDrawStroke = false;
	}

	public void doubleTapAction(MotionEvent event) {
		// Log.d("dtap", "event " + event.getX() + " " + event.getY());
		BitmapPosition position = new BitmapPosition( event.getX(), event.getY());
		for ( int i = m_stickerBitmap.size() - 1 ; i >= 0 ; i-- )
		{
			if ( m_stickerBitmapPosition.get(i).isDraggingMe(position))
			{
				m_stickerBitmap.remove( i);
				m_stickerBitmapPosition.remove( i);
				invalidate();
				break;
			}
		}
	}

	public void setPhotoframeBitmap(Bitmap bmp) {
		if ( bmp != null )
		{
			m_photoframeBitmap = bmp;
			photoframe_rst = new Rect(0, 0, m_photoframeBitmap.getWidth(), m_photoframeBitmap.getHeight());
			invalidate();
		}
	}
}

class BitmapPosition
{
	int x = 0, y = 0;
	int width, height;
	int widthLimit, heightLimit;

	// boost performance by reduced calculation on constants.
	int solvedHalfWidth, solvedHalfHeight;
	int solvedWidthLimit, solvedHeightLimit;
	
	public BitmapPosition( float mouseX, float mouseY)
	{	
		this.x = (int)mouseX;
		this.y = (int)mouseY;
	}
	
	public BitmapPosition( int width, int height, int canvasWidth, int canvasHeight)
	{	
		this.width = width;
		this.height = height;
		this.widthLimit = canvasWidth;
		this.heightLimit = canvasHeight;

		this.solvedHalfWidth = -width / 2;
		this.solvedHalfHeight = -height / 2;
		this.solvedWidthLimit = widthLimit + width / 2;
		this.solvedHeightLimit = heightLimit + height / 2;
	}
	
	public void setX( float x){
		if ( x < solvedHalfWidth )
			this.x = solvedHalfWidth;
		else if ( x + width >= solvedWidthLimit )
			this.x = widthLimit + solvedHalfWidth;
		else
			this.x = (int)x;
	}
	public void setY( float y){
		if ( y < solvedHalfHeight )
			this.y = solvedHalfHeight;
		else if ( y + height >= solvedHeightLimit )
			this.y = heightLimit + solvedHalfHeight;
		else
			this.y = (int)y;
	}
	
	public int getX(){ return x; }
	public int getY(){ return y; }
	
	public boolean isDraggingMe( BitmapPosition position)
	{
		if ( position.x > x && position.x < x + width && position.y > y && position.y < y + height )
			return true;
		else
			return false;
	}
}

